/*
 * Copyright (c) 2012 OptiCare Systems Ltd
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 *
 * Authors: Kevin Martin (kev82@khn.org.uk)
 */
#include <string>
#include <sstream>
#include <stdexcept>
#include <lua5.2/lua.hpp>
#include "luaOE.hpp"
#include <map>
#include <vector>

void luaOE_require(lua_State *l, const std::string &package) {
	lua_getglobal(l, "require");
	if(lua_type(l, -1) != LUA_TFUNCTION) {
		throw std::runtime_error("require isn't a function");
	}
	lua_pushstring(l, package.c_str());
	if(lua_pcall(l, 1, 1, 0) != LUA_OK) {
		luaOE_throwerror(l, "Error calling require in luaOE_require");
	}
	if(lua_type(l, -1) != LUA_TTABLE) {
		throw std::runtime_error("require didn't return table");
	}

	//we leave the table on the top of the stack for the caller
}

void luaOE_throwerror(lua_State *l, std::string err) {
	std::ostringstream oss;
	oss << err;
	if(lua_type(l, -1) != LUA_TSTRING) {
		oss << "<confused, stack top not string>";
	} else {
		oss << lua_tostring(l, -1);
	}
	oss << std::flush;
	lua_pop(l, 1);
	throw std::runtime_error(oss.str());
}

class luaquoter_state
{
private:
	lua_State *l;
	//This can't be copied anyway, but I'll put in the private constructors just in case
	luaquoter_state(const luaquoter_state &) {}
	luaquoter_state &operator=(const luaquoter_state &) { return *this; }
public:
	luaquoter_state() {
		l = luaL_newstate();
		luaL_openlibs(l);
	}
	~luaquoter_state() {
		lua_close(l);
	}

	operator lua_State *() {
		return l;
	}
};

class luaquoter
{
private:
	boost::shared_ptr<luaquoter_state> pl;
public:
	luaquoter(boost::shared_ptr<luaquoter_state> pl) : pl(pl) {}
	std::string operator()(std::string t) {
		lua_State *l = *pl;

		lua_getglobal(l, "string");
		lua_pushstring(l, "format");
		lua_gettable(l, -2);
		lua_remove(l, -2);

		lua_pushstring(l, "%q");
		lua_pushstring(l, t.c_str());
		assert(lua_pcall(l, 2, 1, 0) == 0);
		
		std::string rv = lua_tostring(l, -1);
		lua_pop(l,1);
		return rv;
	}
};

boost::function<std::string (std::string)> luaOE_lua_quoter() {
	boost::shared_ptr<luaquoter_state> s(new luaquoter_state);
	return luaquoter(s);
}

static std::string escapeJsonString(const std::string& input) {
    std::ostringstream ss;
	ss << '"';
    for (std::string::const_iterator iter = input.begin(); iter != input.end(); iter++) {
        switch (*iter) {
            case '"': ss << "\\\""; break;
            case '\\': ss << "\\\\"; break;
            case '/': ss << "\\/"; break;
            case '\b': ss << "\\b"; break;
            case '\f': ss << "\\f"; break;
            case '\n': ss << "\\n"; break;
            case '\r': ss << "\\r"; break;
            case '\t': ss << "\\t"; break;
            default: ss << *iter; break;
        }
    }
	ss << '"';
    return ss.str();
}

boost::function<std::string (std::string)> luaOE_json_quoter() {
	return escapeJsonString;
}


class se_nil : public luaOE_serialized_obj
{
public:
	se_nil() : luaOE_serialized_obj() {}
	virtual ~se_nil() {}
	
	virtual void int_unserializeAsState(lua_State *u) const {
		lua_pushnil(u);
	}
    virtual void int_unserializeAsJSON(std::ostream &os, boost::function<std::string (std::string)> quoter) const {
		os << "\"nil\"";
	}
	virtual void int_unserializeAsLuaString(std::ostream &os, boost::function<std::string (std::string)> quoter) const {
		os << "nil";
	}
	virtual void int_unserializeAsPTree(boost::property_tree::ptree &pt, std::vector<std::string> &position) const {
		//I only add things at a leaf node, so if I donlt add anything it will be in effect, nil
	}
};

class se_boolean : public luaOE_serialized_obj
{
private:
	bool val;
public:
	se_boolean(bool b) : luaOE_serialized_obj(), val(b) {}
	virtual ~se_boolean() {}
	
	virtual void int_unserializeAsState(lua_State *u) const {
		lua_pushboolean(u, val);
	}
    virtual void int_unserializeAsJSON(std::ostream &os, boost::function<std::string (std::string)> quoter) const {
		if(val) os << "true"; else os << "false";
	}
	virtual void int_unserializeAsLuaString(std::ostream &os, boost::function<std::string (std::string)> quoter) const {
		if(val) os << "true"; else os << "false";
	}
	virtual void int_unserializeAsPTree(boost::property_tree::ptree &pt, std::vector<std::string> &position) const {
		std::ostringstream oss;
		assert(!position.empty());
		oss << position[0];
		for(int i=1;i!=position.size();++i) {
			oss << "." << position[i];
		}
		pt.put(oss.str(), val);
	}
};

class se_number : public luaOE_serialized_obj
{
private:
	double val;
public:
	se_number(double d) : luaOE_serialized_obj(), val(d) {}
	virtual ~se_number() {}
	
	virtual void int_unserializeAsState(lua_State *u) const {
		lua_pushnumber(u, val);
	}
    virtual void int_unserializeAsJSON(std::ostream &os, boost::function<std::string (std::string)> quoter) const {
		os << val;
	}
	virtual void int_unserializeAsLuaString(std::ostream &os, boost::function<std::string (std::string)> quoter) const {
		os << val;
	}
	virtual void int_unserializeAsPTree(boost::property_tree::ptree &pt, std::vector<std::string> &position) const {
		std::ostringstream oss;
		assert(!position.empty());
		oss << position[0];
		for(int i=1;i!=position.size();++i) {
			oss << "." << position[i];
		}
		pt.put(oss.str(), val);
	}
};

class se_string : public luaOE_serialized_obj
{
private:
	std::string val;
public:
	se_string(std::string s) : luaOE_serialized_obj(), val(s) {}
	virtual ~se_string() {}
	
	virtual void int_unserializeAsState(lua_State *u) const {
		lua_pushstring(u, val.c_str());
	}
    virtual void int_unserializeAsJSON(std::ostream &os, boost::function<std::string (std::string)> quoter) const {
		os << quoter(val);
	}
	virtual void int_unserializeAsLuaString(std::ostream &os, boost::function<std::string (std::string)> quoter) const {
		os << quoter(val);
	}
	virtual void int_unserializeAsPTree(boost::property_tree::ptree &pt, std::vector<std::string> &position) const {
		std::ostringstream oss;
		assert(!position.empty());
		oss << position[0];
		for(int i=1;i!=position.size();++i) {
			oss << "." << position[i];
		}
		pt.put(oss.str(), val);
	}

	std::string toDataKey() const {
		//should really use a regexp here
		for(int i=0;i!=val.size();++i) {
			if(!isalnum(val[i])) throw std::runtime_error("ptree keys must be alpha numeric strings");
		}
		return val;
	}
};

class se_table : public luaOE_serialized_obj
{
private:
	typedef boost::shared_ptr<luaOE_serialized_obj> SOP;
	std::map<SOP, SOP> elems;
public:
	se_table() : luaOE_serialized_obj() {}
	virtual ~se_table() {}
	
	void addElem(SOP s1, SOP s2) {
		assert(elems.count(s1) == 0);
		elems[s1] = s2;
	}

	virtual void int_unserializeAsState(lua_State *u) const {
		lua_newtable(u);
		for(std::map<SOP, SOP>::const_iterator i=elems.begin();
		 i!=elems.end();++i) {
			int t = lua_gettop(u);
			i->first->int_unserializeAsState(u);
			i->second->int_unserializeAsState(u);
			assert(lua_gettop(u) == t+2);
			lua_settable(u, -3);
		}
	}
    virtual void int_unserializeAsJSON(std::ostream &os, boost::function<std::string (std::string)> quoter) const {
		os << "{\n";
		for(std::map<SOP, SOP>::const_iterator i=elems.begin();
		 i!=elems.end();++i) {
			if(i != elems.begin()) os << ",\n";
			i->first->int_unserializeAsJSON(os, quoter);
			os << " : ";
			i->second->int_unserializeAsJSON(os, quoter);
		}
		os << "\n}";
	}
    virtual void int_unserializeAsLuaString(std::ostream &os, boost::function<std::string (std::string)> quoter) const {
		os << "{\n";
		for(std::map<SOP, SOP>::const_iterator i=elems.begin();
		 i!=elems.end();++i) {
			if(i != elems.begin()) os << ",\n";
			os << "[";
			i->first->int_unserializeAsLuaString(os, quoter);
			os << "] = ";
			i->second->int_unserializeAsLuaString(os, quoter);
		}
		os << "\n}";
	}
	virtual void int_unserializeAsPTree(boost::property_tree::ptree &pt, std::vector<std::string> &position) const {
		for(std::map<SOP, SOP>::const_iterator i=elems.begin();
		 i!=elems.end();++i) {
			//The first element must be a string
			se_string &key = dynamic_cast<se_string &>(*(i->first));
			position.push_back(key.toDataKey());
			i->second->int_unserializeAsPTree(pt, position);
			position.pop_back();
		}
	}
};

void luaOE_serialized_obj::unserializeState(lua_State *s) {
	int_unserializeAsState(s);
}

std::string luaOE_serialized_obj::unserializeJSON() {
	boost::function<std::string (std::string)> jq = luaOE_json_quoter();
	std::ostringstream oss;
	int_unserializeAsJSON(oss, jq);
	return oss.str();
}

std::string luaOE_serialized_obj::unserializeLuaString() {
	boost::function<std::string (std::string)> lq = luaOE_lua_quoter();
	std::ostringstream oss;
	int_unserializeAsLuaString(oss, lq);
	return oss.str();
}
 
boost::property_tree::ptree luaOE_serialized_obj::unserializePTree() {
	boost::property_tree::ptree pt;
	
	std::vector<std::string> position;
	int_unserializeAsPTree(pt, position);
	return pt;
}

boost::shared_ptr<luaOE_serialized_obj> real_serialize(lua_State *s, int stackIdx,
 std::vector<const void *> &tablesSeen);

boost::shared_ptr<luaOE_serialized_obj> real_serialize_table(lua_State *s, int stackIdx,
 std::vector<const void *> &tablesSeen) {
	int tblidx = lua_absindex(s, stackIdx);
	assert(lua_type(s,tblidx) == LUA_TTABLE);
	if(std::find(tablesSeen.begin(), tablesSeen.end(), lua_topointer(s, tblidx))
	 != tablesSeen.end()) {
		throw std::runtime_error("Serialized object not a tree");
	}
	tablesSeen.push_back(lua_topointer(s, tblidx));

	boost::shared_ptr<se_table> t(new se_table);

	lua_pushnil(s);
	while(lua_next(s, tblidx) != 0) {
		t->addElem(real_serialize(s, -2, tablesSeen), real_serialize(s, -1, tablesSeen));
		lua_pop(s, 1);
	}

	return t;
}

boost::shared_ptr<luaOE_serialized_obj> real_serialize(lua_State *s, int stackIdx,
 std::vector<const void *> &tablesSeen) {
	if(lua_type(s, stackIdx) == LUA_TNUMBER) {
		return boost::shared_ptr<luaOE_serialized_obj>(new se_number(lua_tonumber(s, stackIdx)));
	} else if(lua_type(s, stackIdx) == LUA_TSTRING) {
		return boost::shared_ptr<luaOE_serialized_obj>(new se_string(lua_tostring(s, stackIdx)));
	} else if(lua_type(s, stackIdx) == LUA_TTABLE) {
		return real_serialize_table(s, stackIdx, tablesSeen);
	} else if(lua_type(s, stackIdx) == LUA_TNIL) {
		return boost::shared_ptr<luaOE_serialized_obj>(new se_nil());
	} else if(lua_type(s, stackIdx) == LUA_TBOOLEAN) {
		return boost::shared_ptr<luaOE_serialized_obj>(new se_boolean(lua_toboolean(s, stackIdx)));
	} else {
		throw std::runtime_error("Can't serialize type");
	}

	assert("unreachable code" == 0x0);
	throw std::logic_error("unreachable code");
}

boost::shared_ptr<luaOE_serialized_obj> luaOE_serialize(lua_State *s, int stackIdx) {
	std::vector<const void *> tablesSeen;

	return real_serialize(s, stackIdx, tablesSeen);
}
	
boost::shared_ptr<luaOE_serialized_obj> luaOE_serialize_string(const std::string &s) {
	lua_State *l = luaL_newstate();
	luaL_loadstring(l, s.c_str());
	lua_pcall(l, 0, LUA_MULTRET, 0);
	
	int nrv = lua_gettop(l);
	lua_newtable(l);
	for(int i=1;i!=nrv+1;++i) {
		std::ostringstream oss;
		oss << "rv" << i;
		lua_pushstring(l, oss.str().c_str());
		lua_pushvalue(l, i);
		lua_settable(l, -3);
	}

	boost::shared_ptr<luaOE_serialized_obj> rv = luaOE_serialize(l, -1);
	lua_close(l);
	return rv;
}
